// FileWatcher.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"

#define MAX_LISTENERS 50
#define WDIRTAG "%WDIR%"
#define UDIRTAG "%UDIR%"

const char usage[]="\
\nFileWatcher: The program monitors selected directories within given\
\ndirectory for any changes and runs the provided command if any changes\
\nare made. The given command can contain " WDIRTAG " and " UDIRTAG " \
\ntags, that would be subtituted with relative path of directories changed.\
\nThe directories to be monitored are selected based on last modified\
\nfiles in given directory.\
\nOptions:\
\n-d Directory  : Directory to be monitored\
\n-c Command    : Command to execute when any file is modified\
\n-h            : Prints this usage message\
\n-q            : [Optional] Quite Mode\
\n-v            : [Optional] Verbose\
\n-t Timeout    : [Optional] Max Timeout in seconds to wait for\
\n                directory changes. Default value is Infinite.\
\n                Non-Infinite value is useful to accurately monitor \
\n                rarly modified directories.\
\n-m MaxCount   : [Optional] Maximum number of changes before reparsing\
\n                whole tree for file modification times. Higher value\
\n                saves initialization time for big directories but \
\n                tradeoff accuracy. Default value is 1\
\n-r Level      : [Optional] Maximum directory levels to look for files\
\n                to be monitored. Default value is 0 => Infinite Levels."
#if _MSC_VER >= 1500
"\n-i Pattern    : [Optional] Only monitor name matching given regex. \
\n-x Pattern    : [Optional] Don't monitor name matching given regex. "
#endif
"\n-w WatchCount : [Optional] Max directories to monitor. Default is 50\
\n                value = 0=> as allowed by OS. Useful for remote folders\
\n\
\nAuthor : Himanshu Agarwal, Contact: agarwal.himanshu502@gmail.com\
\nSource : https://code.google.com/p/file-watcher/ \
\n";

#include <windows.h>
#include <tchar.h>
#include <strsafe.h>
#include <string>
#include <vector>
#include <set>
#include <iostream>
#include <algorithm>
#if _MSC_VER >= 1500
#include <regex>
#endif
#include <csignal>
#include "getopts.h"
#include "Utils.h"

using namespace std;

namespace
{
	class Signal{
		HANDLE m_hSignal;
		volatile sig_atomic_t m_bQuit;
	public:		
		Signal():m_bQuit(0),m_hSignal(0){}
		BOOL Init(string p_strName)
		{
			m_hSignal = CreateEvent( 
				NULL,               // default security attributes
				TRUE,               // manual-reset event
				FALSE,              // initial state is nonsignaled
				TEXT(p_strName.c_str())  // object name
				); 

			if (m_hSignal == NULL) 
			{ 
				cerr<<"\n"<<"CreateEvent failed. Error: "<<GetLastError();
				return FALSE;
			}
			return TRUE;
		}
		BOOL Set()
		{
			m_bQuit = 1;
			if (! SetEvent(m_hSignal) ) 
			{
				cerr<<"\n"<<"SetEvent failed. Error: "<<GetLastError();
				return FALSE;
			}
			return TRUE;
		}
		operator BOOL() const {	return m_bQuit;	}
		operator HANDLE() const {return m_hSignal; }
		~Signal()
		{
			CloseHandle(m_hSignal);
		}
	};
    Signal g_sigQuit;
    void signal_handler(int sig)
    {
		cout<<"\n"<<"Recieved Signal: "<<sig;
        signal(sig, signal_handler);
        g_sigQuit.Set();
    }
}

class DirectoryToMonitor
{
public:
	DirectoryToMonitor(const string &p_strPath, const string &p_strCommandToExec, const Signal &p_sigQuit)
		:m_sigQuit(p_sigQuit)
	{
        m_fsoRootDir.m_strPath			= p_strPath;
        m_fsoRootDir.m_bIsDirectory		= TRUE;
		m_fsoRootDir.m_pParentDir		= NULL;
		m_uMaxDirMonitor				= MAX_LISTENERS;
		m_strCommandToExec				= p_strCommandToExec;
		m_bQuiet						= FALSE;
		m_bVerbose						= FALSE;
		m_dwWaitTimeout					= INFINITE;
		m_uReIndexLoop					= 0;
	}

	void SetLimit(unsigned int p_limit)		{m_uMaxDirMonitor=p_limit;}
	void SetQuiet(BOOL val)					{m_bQuiet=val;}
	void SetVerbose(BOOL val)				{m_bVerbose=val;}
	void SetTimeout(DWORD val)				{m_dwWaitTimeout=val*100;}
	void SetMinLoops(unsigned int val)		{if(val >= 1) m_uReIndexLoop=val;}
	void SetRecusionLevel(unsigned int val)	{m_uMaxDepth=val;}
	void SetInclRegex(string val)			{m_strInclRegEx=val;}
	void SetExclRegex(string val)			{m_strExclRegEx=val;}

	BOOL Start();
	
	~DirectoryToMonitor()
	{
		Clear();
	}

private:
    struct FileSysObj{
		typedef FileSysObj* Ptr;
		struct TimeComparator
		{
			bool operator()(Ptr x, Ptr y) const
			{ 
				int res = CompareFileTime(&(x->m_ftLastModifiedTime), &(y->m_ftLastModifiedTime));
				if(res == 1)
					return true;
				else if(res == -1)
					return false;
				else
					return x->m_strPath > y->m_strPath;
			}
		};

		struct PathComparator : public std::unary_function<Ptr, bool>
		{
			PathComparator(const std::string &p_strFilePath) : m_strFilePath(p_strFilePath) {}
			bool operator() (Ptr &arg) const { return(arg->m_strPath == m_strFilePath); }
			std::string m_strFilePath;
		};
		typedef set<Ptr, FileSysObj::TimeComparator> ModTimeIndexSet;
		typedef vector<Ptr> List;		

		string          m_strPath;
	    FILETIME        m_ftLastModifiedTime;
	    BOOL            m_bIsDirectory; //TRUE=Directory
	    List			m_vecChildren;
	    Ptr				m_pParentDir;
    };    

	FileSysObj						m_fsoRootDir;	
	FileSysObj::ModTimeIndexSet		m_setModTimeSortedFiles;
	string							m_strCommandToExec;
	unsigned int					m_uMaxDirMonitor;
	BOOL							m_bQuiet;
	BOOL							m_bVerbose;
	DWORD							m_dwWaitTimeout;
	unsigned int					m_uReIndexLoop;
	unsigned int					m_uMaxDepth;
	string							m_strInclRegEx;
	string							m_strExclRegEx;
	const Signal					&m_sigQuit;

	virtual void FileChanged(FileSysObj &p_fsoChangedDir) const
	{
		string rel_path;
		if(p_fsoChangedDir.m_strPath.length() > m_fsoRootDir.m_strPath.length())
			rel_path = p_fsoChangedDir.m_strPath.substr(m_fsoRootDir.m_strPath.length()+1);
        string mod_command = str_replace(m_strCommandToExec, WDIRTAG, rel_path);
		std::replace( rel_path.begin(), rel_path.end(), '\\', '/'); 
        mod_command = str_replace(mod_command, UDIRTAG, rel_path);
		if(m_bVerbose && !m_bQuiet) cout<<"\n"<<"Executing :"<<mod_command;
		system(mod_command.c_str());
	}

	void Clear()
	{
		for(FileSysObj::ModTimeIndexSet::iterator it = m_setModTimeSortedFiles.begin()
			;it != m_setModTimeSortedFiles.end(); it++)
			delete *(it);
		m_setModTimeSortedFiles.clear();
		m_fsoRootDir.m_vecChildren.clear();
	}

	BOOL Populate(FileSysObj &p_pDirToParse, int p_nDirDepth);
};

BOOL DirectoryToMonitor::Start()
{
    DWORD dwWaitStatus; 
	HANDLE hTemp;
	vector<HANDLE> vecChangeHandles;
	vecChangeHandles.reserve(MAX_LISTENERS+1);
	unsigned int uLoopCount = m_uReIndexLoop;

	if(!m_bQuiet) cout<<"\n"<<"Initializing...";
		
	while(!((BOOL)m_sigQuit))
	{
		if(uLoopCount == m_uReIndexLoop)
		{
			if(!m_bQuiet) cout<<"\n"<<"Populating files list...";
			if(GetLastWriteTime(m_fsoRootDir.m_strPath.c_str()
				, m_fsoRootDir.m_ftLastModifiedTime, m_fsoRootDir.m_bIsDirectory) == FALSE)
			{
				cerr<<"\n"<<"GetLastWriteTime failed."<<", File: "<<m_fsoRootDir.m_strPath.c_str();
				return FALSE;
			}

			if(Populate(m_fsoRootDir, m_uMaxDepth==0?-1:m_uMaxDepth) == FALSE)
			{
				cerr<<"\n"<<"Populate failed."<<", File: "<<m_fsoRootDir.m_strPath.c_str();
				return FALSE;
			}
		}

        if(!m_bQuiet) cout<<"\n"<<"Setting up listner events...";

		FileSysObj::List vecMonitoredDirs;    unsigned int uLastIndex=0;
		vecMonitoredDirs.reserve(MAX_LISTENERS);

		for(FileSysObj::ModTimeIndexSet::iterator it = m_setModTimeSortedFiles.begin()
            ;it != m_setModTimeSortedFiles.end() 
                && uLastIndex<(m_uMaxDirMonitor==0?MAX_LISTENERS:m_uMaxDirMonitor)
                && uLastIndex<MAX_LISTENERS
            ; it++)
		{
			const FileSysObj::Ptr pFileSysObj = *(it);
			if(pFileSysObj->m_bIsDirectory == TRUE) continue;
			string strDirPath = ((pFileSysObj->m_bIsDirectory)
											?pFileSysObj
											:pFileSysObj->m_pParentDir)
											->m_strPath;
				
            //Already watched...
            if(std::find_if(vecMonitoredDirs.begin()
                            , vecMonitoredDirs.end()
                            , FileSysObj::PathComparator(strDirPath)) 
							!= vecMonitoredDirs.end()) continue;
#if _MSC_VER >= 1500
			//Only include files matching regular expression
			if(!m_strInclRegEx.empty() && !regex_match (pFileSysObj->m_strPath
														, regex(m_strInclRegEx)))
				continue;
			//Dont include files matching regular expression
			if(!m_strExclRegEx.empty() && regex_match (pFileSysObj->m_strPath
														, regex(m_strExclRegEx)))
				continue;
#endif				
			hTemp = FindFirstChangeNotification(strDirPath.c_str()
                                                                            , FALSE
                                                                            , FILE_NOTIFY_CHANGE_DIR_NAME
													                        |FILE_NOTIFY_CHANGE_SIZE
													                        |FILE_NOTIFY_CHANGE_LAST_WRITE
													                        |FILE_NOTIFY_CHANGE_FILE_NAME); 
 
			if (hTemp == INVALID_HANDLE_VALUE || hTemp == NULL) 
			{
				DWORD error = GetLastError();
				if(ERROR_TOO_MANY_CMDS == error && m_uMaxDirMonitor == 0)
				{
					if(m_bVerbose) cout<<"\n"<<"Netbios Limit reached after "<<uLastIndex;
					break;
				}
                cerr<<"\n"<<"FindFirstChangeNotification failed. Error: "<<error
                    <<", Directory: "<<strDirPath
                    <<", Handle Count: "<<uLastIndex;
				return FALSE;
			}
			vecChangeHandles.push_back(hTemp);
			vecMonitoredDirs.push_back(((pFileSysObj->m_bIsDirectory)?pFileSysObj:pFileSysObj->m_pParentDir));
			uLastIndex++;
		}
		//Adding Quit Signal
		vecChangeHandles.push_back((HANDLE)m_sigQuit);

        //Check if there is anthing to watch?
		if(uLastIndex > 0)
		{
            if(!m_bQuiet) cout<<"\n"<<"Waiting for changes("<<uLastIndex<<")...";
			if(m_bVerbose)
			{
				if(!m_bQuiet) cout<<"\n"<<"Monitoring following Directories...";
				for(unsigned int i=0;i<vecMonitoredDirs.size();i++)
					if(!m_bQuiet) cout<<"\n"<<i<<">"<<vecMonitoredDirs[i]->m_strPath;
			}
			dwWaitStatus = WaitForMultipleObjects(uLastIndex+1, &vecChangeHandles[0], FALSE, m_dwWaitTimeout); 
			if(WAIT_FAILED == dwWaitStatus)
			{
				cerr<<"\n"<<"WaitForMultipleObjects failed. Error: "<<GetLastError();
				return FALSE;
			}
			if(WAIT_TIMEOUT != dwWaitStatus && (dwWaitStatus - WAIT_OBJECT_0) < uLastIndex)
			{
				if(!m_bQuiet) cout<<"\n"<<"Something changed in : "
									<<(*(vecMonitoredDirs[dwWaitStatus - WAIT_OBJECT_0])).m_strPath;
				FileChanged(*(vecMonitoredDirs[dwWaitStatus - WAIT_OBJECT_0]));
			}

            //Closing all the handles
			for(unsigned int index=0;index<uLastIndex;index++)
				FindCloseChangeNotification(vecChangeHandles[index]);
			vecChangeHandles.clear();
			vecChangeHandles.reserve(MAX_LISTENERS+1);
		}

        if(!m_bQuiet) cout<<"\n"<<"Winding up...";

		if(uLoopCount < m_uReIndexLoop) uLoopCount++;
		else uLoopCount = 1;
			
		if(uLoopCount == m_uReIndexLoop) Clear();
	}
	return TRUE;
}

BOOL DirectoryToMonitor::Populate(FileSysObj &p_pDirToParse, int p_nDirDepth)
{
	if(p_nDirDepth == 0) 
		return TRUE;
	p_nDirDepth--;

	WIN32_FIND_DATA ffd;
	FileSysObj::Ptr pfsoChild;
	HANDLE hFind			= INVALID_HANDLE_VALUE;
	DWORD dwError			= 0;		
	string strSearchPath	= p_pDirToParse.m_strPath + "\\*";

	hFind = FindFirstFile(strSearchPath.c_str(), &ffd);
	if (INVALID_HANDLE_VALUE == hFind) 
	{
		cerr<<"\n"<<"FindFirstFile failed. Error: "<<GetLastError()
			<<", File: "<<strSearchPath.c_str();
		return FALSE;
	}

	do
	{
		//Ignore self and parent directory
		if(!(strcmp(ffd.cFileName,".")==0 || strcmp(ffd.cFileName,"..")==0)) 
		{
			pfsoChild = new FileSysObj();
			pfsoChild->m_strPath = p_pDirToParse.m_strPath + "\\" + ffd.cFileName;
			pfsoChild->m_bIsDirectory = (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)?TRUE:FALSE;
			pfsoChild->m_pParentDir = &p_pDirToParse;

			if(GetLastWriteTime(pfsoChild->m_strPath.c_str()
								, pfsoChild->m_ftLastModifiedTime
								, pfsoChild->m_bIsDirectory) == FALSE)
			{
				cerr<<"\n"<<"GetLastWriteTime failed. Error: "<<GetLastError()
					<<", File: "<<pfsoChild->m_strPath.c_str();
				return FALSE;
			}
			p_pDirToParse.m_vecChildren.push_back(pfsoChild);
			m_setModTimeSortedFiles.insert(pfsoChild);

			if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
			{
				if(Populate(*pfsoChild, p_nDirDepth) == FALSE)
				{
					cerr<<"\n"<<"Populate failed"<<", File: "<<pfsoChild->m_strPath.c_str();
					return FALSE;
				}
			}
		}
	}
	while (FindNextFile(hFind, &ffd) != 0);
 
	dwError = GetLastError();
	if (dwError != ERROR_NO_MORE_FILES) 
	{
        cerr<<"\n"<<"FindNextFile failed. Error: "<<", File: "<<strSearchPath.c_str();
		return FALSE;
	}
		
	FindClose(hFind);
	return TRUE;
}

int main(int argc, char* argv[])
{
	if(g_sigQuit.Init("QuitEvent") == FALSE)
		return -1;

	signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);
#ifdef SIGBREAK
    signal(SIGBREAK, signal_handler);
#endif

	int opt;
	string directory;
	string command;
    while ((opt = getopt(argc,argv,"d:c:t:m:r:i:x:w:hqv")) != EOF)
    switch(opt)
    {
        case 'd': directory = optarg; break;
        case 'c': command = optarg; break;
    }

	if(directory.empty() || command.empty())
	{
		cout<<usage; 
		exit(-1);
	}
	DirectoryToMonitor monitor(directory, command, g_sigQuit);
	
	opterr = 1;	optind = 1; sp = 1;
    while ((opt = getopt(argc,argv,"d:c:t:m:r:i:x:w:hqv")) != EOF)
    switch(opt)
    {
		case 'd': break;
        case 'c': break;
        case 't': monitor.SetTimeout(atoi(optarg)); break;
        case 'm': monitor.SetMinLoops(atoi(optarg)); break;
		case 'r': monitor.SetRecusionLevel(atoi(optarg)); break;
		case 'i': monitor.SetInclRegex(optarg); break;
		case 'x': monitor.SetExclRegex(optarg); break;
		case 'w': monitor.SetLimit(atoi(optarg)); break;
		case 'h': cout<<usage; exit(0);
		case 'q': monitor.SetQuiet(TRUE); break;
		case 'v': monitor.SetVerbose(TRUE); break;
		case '?': cout<<usage; exit(-1);
        default: cout<<endl; abort();
    }
	
	if(monitor.Start() == FALSE) return -1;
	return 0;
}
